/*
    Copyright 2009 Spencer B. Koehler, Ph.D.

    This file is part of the Brillig Toolkit.

    The Brillig Toolkit is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    The Brillig Toolkit is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with The Brillig Toolkit.  If not, see <http://www.gnu.org/licenses/>.
*/
package org.brillig.xml;


import org.brillig.util.tree.Tree;
import org.brillig.xml.XmlLite;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A utility class to deal with html peculiarities.
 * <p>
 * @author Spence Koehler
 */
public class HtmlHelper {
  
  private static final boolean IGNORE_HEADINGS_456 = false;

  /**
   * Default tags (whose text) to ignore in the tree.
   */
  public static final String[] DEFAULT_IGNORE_TAG_STRINGS = new String[] {
    /*"meta", "link",*/ "style", "script", /*"form",*/ "select", "input", "option",
    "optgroup", "textarea", "button",
  };
  public static final Set<String> DEFAULT_IGNORE_TAGS = new HashSet<String>();
  static {
    for (String dits : DEFAULT_IGNORE_TAG_STRINGS) {
      DEFAULT_IGNORE_TAGS.add(dits);
    }
  }

//   /** Text found under these tags cannot be consecutive with the next text node. */
//   public static final String[] DEFAULT_NON_CONSECUTIVE_TEXT_TAGS = new String[] {
//     "title", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "address", "code",
//     "li", "dt", "dd", "caption", "thead", "tfoot", "th"
//   };

  /**
   * Default tag names whose attributes should be ignored when creating
   * a key to match tags. All other tag attributes will be included in
   * generating a key.
   */
  public static final String[] DEFAULT_IGNORE_TAG_ATTRIBUTES = new String[] {
    "a", "image", "table", "tr", "td",
  };

  /** Any text found under these tags are considered headings. */
  public static final String[] DEFAULT_HEADING_TAGS = new String[] {
    // since these are also non_consecutive_text_tags, they  are always headings
    "title", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "thead", "th",

    // these are only considered as heading when found over non consecutive text
    "font@size", "em", "b", "strong"
  };

  public static int MIN_USABLE_STRENGTH = 1;
  public static int MAX_STRENGTH = 7;

  /**
   * Default mappings from heading values to heading 'strength':
   * <p>
   *        h       h1   h2   h3        h4   h5  h6  <p>
   *   size-N        6    5    4    3    2    1   0  <p>
   *   size-%      200  150  120  100   80   70  60  <p>
   *                hr                    thead  th  <p>
   *                                             em  <p>
   *                                         strong  <p>
   *                                              b  <p>
   *          title
   * strength   7    6    5    4    0    3    2   1  <p>
   * 
   */
  public static final Map<String, Integer> VALUE_TO_STRENGTH = new HashMap<String, Integer>();
  static {
    if (IGNORE_HEADINGS_456) {
      VALUE_TO_STRENGTH.put("h1", 6);
      VALUE_TO_STRENGTH.put("h2", 5);
      VALUE_TO_STRENGTH.put("h3", 4);
      VALUE_TO_STRENGTH.put("h4", 3);
      VALUE_TO_STRENGTH.put("h5", 2);
      VALUE_TO_STRENGTH.put("h6", 1);
      VALUE_TO_STRENGTH.put("hr", 6);

      VALUE_TO_STRENGTH.put("200%", 6);
      VALUE_TO_STRENGTH.put("150%", 5);
      VALUE_TO_STRENGTH.put("120%", 4);
      VALUE_TO_STRENGTH.put("100%", 0);
      VALUE_TO_STRENGTH.put( "80%", 3);
      VALUE_TO_STRENGTH.put( "70%", 2);
      VALUE_TO_STRENGTH.put( "60%", 1);

      VALUE_TO_STRENGTH.put("+3", 6);
      VALUE_TO_STRENGTH.put("+2", 5);
      VALUE_TO_STRENGTH.put("+1", 4);
      VALUE_TO_STRENGTH.put("1", 0);
      VALUE_TO_STRENGTH.put("-1", 2);
      VALUE_TO_STRENGTH.put("-2", 1);

      VALUE_TO_STRENGTH.put("thead", 2);
      VALUE_TO_STRENGTH.put("th", 1);
      VALUE_TO_STRENGTH.put("em", 1);
      VALUE_TO_STRENGTH.put("strong", 1);
      VALUE_TO_STRENGTH.put("b", 1);

      VALUE_TO_STRENGTH.put("title", 7);

      MIN_USABLE_STRENGTH = 3;
      MAX_STRENGTH = 7;
    }
    else {
      VALUE_TO_STRENGTH.put("h1", 6);
      VALUE_TO_STRENGTH.put("h2", 5);
      VALUE_TO_STRENGTH.put("h3", 4);
      VALUE_TO_STRENGTH.put("h4", 3);
      VALUE_TO_STRENGTH.put("h5", 2);
      VALUE_TO_STRENGTH.put("h6", 1);
      VALUE_TO_STRENGTH.put("hr", 6);

      VALUE_TO_STRENGTH.put("200%", 6);
      VALUE_TO_STRENGTH.put("150%", 5);
      VALUE_TO_STRENGTH.put("120%", 4);
      VALUE_TO_STRENGTH.put("100%", 0);
      VALUE_TO_STRENGTH.put( "80%", 3);
      VALUE_TO_STRENGTH.put( "70%", 2);
      VALUE_TO_STRENGTH.put( "60%", 1);

      VALUE_TO_STRENGTH.put("+3", 6);
      VALUE_TO_STRENGTH.put("+2", 5);
      VALUE_TO_STRENGTH.put("+1", 4);
      VALUE_TO_STRENGTH.put("1", 0);
      VALUE_TO_STRENGTH.put("-1", 0);
      VALUE_TO_STRENGTH.put("-2", 0);

      VALUE_TO_STRENGTH.put("thead", 0);
      VALUE_TO_STRENGTH.put("th", 0);
      VALUE_TO_STRENGTH.put("em", 0);
      VALUE_TO_STRENGTH.put("strong", 0);
      VALUE_TO_STRENGTH.put("b", 0);

      VALUE_TO_STRENGTH.put("title", 7);

      MIN_USABLE_STRENGTH = 1;
      MAX_STRENGTH = 7;
    }
  }


  private Set<String> ignoreTags;
  private Set<String> headingTags;
  private Map<String, String> tag2attribute;  // i.e. font@size -> font -> size
  private Map<String, Integer> hVal2Strength;
  private Set<String> ignoreTagAttributes;

  /**
   * Construct a new block builder using defaults.
   */
  public HtmlHelper() {
    this(DEFAULT_IGNORE_TAG_STRINGS, DEFAULT_HEADING_TAGS, VALUE_TO_STRENGTH, DEFAULT_IGNORE_TAG_ATTRIBUTES);
  }

  /**
   * Construct a new block builder with the given settings.
   */
  public HtmlHelper(String[] ignoreTags,
                    String[] headingTags,
                    Map<String, Integer> hVal2Strength,
                    String[] ignoreTagAttributes) {

    this.ignoreTags = XmlLite.buildTagSet(ignoreTags);
    buildHeadingTags(headingTags);
    this.hVal2Strength = hVal2Strength;
    this.ignoreTagAttributes = XmlLite.buildTagSet(ignoreTagAttributes);
  }
  
  private final void buildHeadingTags(String[] headingTags) {
    this.headingTags = new HashSet<String>();
    this.tag2attribute = new HashMap<String, String>();

    for (String headingTag : headingTags) {
      final String[] tagAtt = headingTag.split("@");
      this.headingTags.add(tagAtt[0]);
      if (tagAtt.length == 2) this.tag2attribute.put(tagAtt[0], tagAtt[1]);
    }
  }

  /**
   * Compute the maximum heading strength of the path between deep and shallow
   * nodes (inclusive).
   */
  public final int computeHeadingStrength(Tree<XmlLite.Data> deepNode, Tree<XmlLite.Data> shallowNode) {
    int result = 0;

    while (deepNode != null) {
      final int curResult = computeHeadingStrength(deepNode);
      if (curResult > result) result = curResult;

      if (deepNode == shallowNode) break;
      deepNode = deepNode.getParent();
    }

    return result;
  }

  // take a peek at the text and see if it is a manual divider (== hr)
  // (i.e. if the user wrote a string of dashes)
  public final int manualHeadingStrength(Tree<XmlLite.Data> textNode) {
    int result = 0;
    if (isManualDivider(textNode)) {
      result = hVal2Strength.get("hr");
    }
    return result;
  }

  public final int computeHeadingStrength(Tree<XmlLite.Data> headingNode) {
    int result = 0;

    final XmlLite.Tag tag = headingNode.getData().asTag();
    final String att = tag2attribute.get(tag.name);  // check for attribute (i.e. font@size=)
    final String value = (att == null) ? tag.name : tag.getAttribute(att);
    if (value != null) {  // found attribute or using tag name
      final Integer strength = hVal2Strength.get(value);
      if (strength != null) result = strength;
    }

    return result;
  }

  private final boolean isManualDivider(Tree<XmlLite.Data> node) {
    boolean result = false;

    final Tree<XmlLite.Data> textNode = diveForText(node);
    if (textNode != null) {
      final String text = textNode.getData().asText().text;
      if (text.length() > 3) {
        result = true;
        for (int i = 0; i < 4; ++i) {
          if (text.charAt(i) != '-') {
            result = false;
            break;
          }
        }
      }
    }

    return result;
  }

  public static final Tree<XmlLite.Data> diveForText(Tree<XmlLite.Data> xmlNode) {
    Tree<XmlLite.Data> textNode = null;
    final List<Tree<XmlLite.Data>> children = xmlNode.getChildren();
    if (children == null || children.size() == 0) {
      if (xmlNode.getData().asText() != null) {
        textNode = xmlNode;
      }
    }
    else if (children.size() == 1) {
      final Tree<XmlLite.Data> child = children.get(0);
      textNode = diveForText(child);
    }
    return textNode;
  }

  /**
   * Determine whether the given node has a tag designated to be ignored.
   */
  public final boolean ignoreTag(Tree<XmlLite.Data> node) {
    boolean result = false;

    final XmlLite.Tag tag = node.getData().asTag();

    if (tag != null) {
      result = ignoreTags.contains(tag.name);
    }

    return result;
  }

  /**
   * Determine whether the given tag is designated to be ignored.
   */
  public final boolean ignoreTag(XmlLite.Tag tag) {
    boolean result = false;

    if (tag != null) {
      result = ignoreTags.contains(tag.name);
    }

    return result;
  }

  public final boolean ignoreTag(String tagName) {
    return ignoreTags.contains(tagName);
  }

  public final String getTagKey(XmlLite.Tag tag) {
    String result = null;

    if (tag != null) {
      result = buildTagKey(tag, ignoreTagAttributes.contains(tag.name));
    }

    return result;
  }

  protected static final String buildTagKey(XmlLite.Tag tag, boolean ignoreAttributes) {
    if (ignoreAttributes) return tag.name;

    final StringBuilder result = new StringBuilder();

    result.append(tag.name);
    for (Map.Entry<String, String> attval : tag.getAttributeEntries()) {
      result.append(' ').
        append(attval.getKey()).
        append('=').
        append(attval.getValue());
    }

    return result.toString();
  }
}
